/**
 * 
 */
package com.gr.staffpm.charts;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.apache.wicket.ResourceReference;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.MarkupStream;
import org.apache.wicket.markup.html.IHeaderContributor;
import org.apache.wicket.markup.html.IHeaderResponse;
import org.apache.wicket.markup.html.WebComponent;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.markup.parser.XmlTag;
import org.apache.wicket.model.IModel;
import org.apache.wicket.protocol.http.WebRequestCycle;
import org.apache.wicket.protocol.http.request.WebClientInfo;
import org.joda.time.DateTime;
import org.wicketstuff.flot.Color;
import org.wicketstuff.flot.DataSet;
import org.wicketstuff.flot.LegendPosition;
import org.wicketstuff.flot.TickCollection;

import com.gr.staffpm.js.StaffPMJSScope;


/**
 * @author Graham Rhodes 5 Apr 2011 19:55:35
 */
public class FlotPanel extends Panel implements IHeaderContributor {

    private static final long serialVersionUID = 1L;
    private final Map<String, Map<String, Object>> options = new HashMap<String, Map<String, Object>>();
    private boolean showTooltip = false;

    @Override
    public void renderHead(IHeaderResponse response) {
        response.renderJavascriptReference(new ResourceReference(StaffPMJSScope.class, "flot/jquery.flot.min.js"));
        WebClientInfo clientInfo = (WebClientInfo) WebRequestCycle.get().getClientInfo();
        if (clientInfo.getUserAgent().contains("MSIE")) {
            response.renderJavascriptReference(new ResourceReference(StaffPMJSScope.class, "flot/excanvas.min.js"));
        }

    }

    public FlotPanel(final String id, final IModel<List<Series>> model) {
        this(id, 0, 0, model);
    }

    public FlotPanel(String id, final int width, final int height, final IModel<List<Series>> model) {
        super(id, model);

        WebMarkupContainer panel = new WebMarkupContainer("panel") {

            private static final long serialVersionUID = 1L;

            @Override
            protected void onComponentTag(ComponentTag tag) {
                String style = width == 0 ? "width: 500px; " : "width: " + width + "px; ";
                style += height == 0 ? "height: 400px;" : "height: " + height + "px;";
                tag.getAttributes().put("style", style);
            }
        };

        String[] optionsKeys = { "lines", "points", "legend", "xaxis", "yaxis", "x2axis", "y2axis", "selection", "grid" };

        for (String key : optionsKeys)
            options.put(key, new HashMap<String, Object>());

        options.get("selection").put("mode", "xy");
        options.get("grid").put("hoverable", true);
        options.get("grid").put("clickable", true);

        // JN - commented out to allow autoscaling if not set
        //options.get("yaxis").put("min", 0);
        //options.get("yaxis").put("max", 15);

        // This custom component fills the <script> tag with the script returned by getFlotScript().
        panel.add(new WebComponent("flotScript") {

            private static final long serialVersionUID = 1L;

            @Override
            protected void onComponentTagBody(final MarkupStream markupStream, final ComponentTag openTag) {
                replaceComponentTagBody(markupStream, openTag, getFlotScript());
            }

            @Override
            protected void onComponentTag(ComponentTag tag) {
                super.onComponentTag(tag);
                // always transform the tag to <span></span> so even labels defined as <span/> render
                tag.setType(XmlTag.OPEN);
            }
        });
        add(panel);
    }

    /**
     * Returns Javascript code which renders the flot graph.
     * @throws RuntimeException if FlotPanel.js cannot be loaded.
     */
    private String getFlotScript() {
        try {
            final String str = getResourceContents(FlotPanel.class, "js/FlotPanel.js");

            //final String options = "{ lines: { show: true }, points: { show: true }, selection: { mode: \"xy\" }, grid: { hoverable: true, clickable: true }, xaxis: { mode: 'time', min: 1243807200000, max: 1246312800000, timeformat: '%d.%m.%y' }, yaxis: { min: 0, max: 15 }}";

            @SuppressWarnings("unchecked")
            final List<Series> data = (List<Series>) getDefaultModelObject();

            final StringBuffer strData = new StringBuffer();
            strData.append("[");
            for (Series dataEntry : data) {
                strData.append(dataEntry.toString());
                strData.append(", ");
            }
            if (data.size() > 0)
                strData.setLength(strData.length() - 2);
            strData.append("]");

            String strOptions = mapToString(options);

            return String.format(str, strData, strOptions, (showTooltip ? "true" : "false"));
        } catch (IOException e) {
            throw new RuntimeException("Unable to load FlotPanel.js", e);
        }
    }

    private static String mapToString(final Map<?, ?> map) {
        final StringBuilder str = new StringBuilder();

        str.append("{");

        Set<?> keySet = map.keySet();

        for (Object key : keySet) {
            str.append(key);
            str.append(": ");

            Object value = map.get(key);

            if (Map.class.isAssignableFrom(value.getClass()))
                str.append(mapToString((Map<?, ?>) value));
            else if (value.getClass().isArray()) {
                Object[] array = (Object[]) value;
                str.append("[");
                for (Object obj : array) {
                    boolean valueIsString = obj.getClass().equals(String.class);

                    if (valueIsString)
                        str.append("\"");
                    str.append(obj);
                    if (valueIsString)
                        str.append("\"");
                    str.append(", ");
                }
                str.delete(str.length() - 2, str.length());
                str.append("]");
            } else if (value.getClass().equals(DateTime.class)) {
                DataSet set = new DataSet((DateTime) value, 0);
                str.append(String.format(Locale.US, "%f", set.getX()));
            } else {
                boolean valueIsString = value.getClass().equals(String.class);

                if (valueIsString)
                    str.append("\"");
                str.append(value);
                if (valueIsString)
                    str.append("\"");
            }

            str.append(", ");
        }

        if (keySet.size() > 0)
            str.setLength(str.length() - 2);

        str.append("}");

        return str.toString();
    }

    public void setAxisMinTimeTickSizeX(int size, String unit) {
        Object[] minTickSize = { size, unit };
        options.get("xaxis").put("minTickSize", minTickSize);
    }

    public void setAxisTimeModeX(boolean timeMode) {
        String mode = timeMode ? "time" : "null";
        options.get("xaxis").put("mode", mode);
    }

    public void setAxisTimeformatX(String timeFormat) {
        options.get("xaxis").put("timeformat", timeFormat);
    }

    public void setAxisMinX(Double axisMinX) {
        options.get("xaxis").put("min", axisMinX);
    }

    public void setAxisMinX(DateTime time) {
        options.get("xaxis").put("min", time);
    }

    public Double getAxisMinX() {
        return (Double) options.get("xaxis").get("min");
    }

    public void setAxisMinY(Double axisMinY) {
        options.get("yaxis").put("min", axisMinY);
    }

    public Double getAxisMinY() {
        return (Double) options.get("yaxis").get("min");
    }

    public void setAxisMaxX(Double axisMaxX) {
        options.get("xaxis").put("max", axisMaxX);
    }

    public void setAxisMaxX(DateTime time) {
        options.get("xaxis").put("max", time);
    }

    public Double getAxisMaxX() {
        return (Double) options.get("xaxis").get("max");
    }

    public void setAxisMaxY(Double axisMaxY) {
        options.get("yaxis").put("max", axisMaxY);
    }

    public Double getAxisMaxY() {
        return (Double) options.get("yaxis").get("max");
    }

    public void setAxisTicksX(TickCollection tickCollection) {
        options.get("xaxis").put("ticks", tickCollection);
    }

    public TickCollection getAxisTicksX() {
        return (TickCollection) options.get("xaxis").get("ticks");
    }

    public void setAxisTicksY(TickCollection tickCollection) {
        options.get("yaxis").put("ticks", tickCollection);
    }

    public TickCollection getAxisTicksY() {
        return (TickCollection) options.get("yaxis").get("ticks");
    }

    public void setLegendPosition(LegendPosition position) {
        options.get("legend").put("position", position.getPosition());
    }

    public LegendPosition getLegendPosition() {
        return LegendPosition.toLegendPosition((String) options.get("legend").get("position"));
    }

    public void setLegendColumns(int noColumns) {
        options.get("legend").put("noColumns", noColumns);
    }

    public int getLegendColumns() {
        return (Integer) options.get("legend").get("noColumns");
    }

    public void setForegroundColor(Color color) {
        options.get("grid").put("color", color);
    }

    public Color getForegroundColor() {
        return (Color) options.get("grid").get("color");
    }

    public void setBackgroundColor(Color color) {
        options.get("grid").put("backgroundColor", color);
    }

    public Color getBackgroundColor() {
        return (Color) options.get("grid").get("backgroundColor");
    }

    public void setTickColor(Color color) {
        options.get("grid").put("tickColor", color);
    }

    public Color getTickColor() {
        return (Color) options.get("grid").get("tickColor");
    }

    public void setClickable(boolean clickable) {
        options.get("grid").put("clickable", clickable);
    }

    public boolean getClickable() {
        return (Boolean) options.get("grid").get("clickable");
    }

    public boolean getHoverable() {
        return (Boolean) options.get("grid").get("hoverable");
    }

    public void setHoverable(boolean hoverable) {
        options.get("grid").put("hoverable", hoverable);
    }

    public boolean getPointsVisible() {
        return (Boolean) options.get("points").get("show");
    }

    public void setPointsVisible(boolean visible) {
        options.get("points").put("show", visible);
    }

    public void showTooltip(boolean showTooltip) {
        this.showTooltip = showTooltip;
        if (showTooltip)
            setHoverable(true);
    }

    /**
     * Returns the contents of the given resource.
     * @param location The resource path.
     * @return The resource's content.
     * @throws FileNotFoundException If the resource cannot be found.
     * @throws IOException If anything goes wrong while reading the resource.
     */
    private static String getResourceContents(Class<?> clazz, String location) throws IOException {
        InputStream in = clazz.getResourceAsStream(location);
        if (in == null)
            throw new FileNotFoundException("'" + location + "' not found.");
        return getStreamContents(new InputStreamReader(in));
    }

    /**
     * Returns the complete content of the given stream.
     * @throws IOException If anything goes wrong while reading the stream.
     */
    private static String getStreamContents(InputStreamReader inputStreamReader) throws IOException {
        StringBuffer str = new StringBuffer();
        BufferedReader in = new BufferedReader(inputStreamReader);

        while (true) {
            String line = in.readLine();
            if (line == null)
                break;
            str.append(line);
            str.append("\n");
        }

        return str.toString();
    }

}
